{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Задание 2.1 - Нейронные сети\n",
    "\n",
    "В этом задании вы реализуете и натренируете настоящую нейроную сеть своими руками!\n",
    "\n",
    "В некотором смысле это будет расширением прошлого задания - нам нужно просто составить несколько линейных классификаторов вместе!\n",
    "\n",
    "<img src=\"https://i.redd.it/n9fgba8b0qr01.png\" alt=\"Stack_more_layers\" width=\"400px\"/>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "%matplotlib inline\n",
    "\n",
    "%load_ext autoreload\n",
    "%autoreload 2"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from dataset import load_svhn, random_split_train_val\n",
    "from gradient_check import check_layer_gradient, check_layer_param_gradient, check_model_gradient\n",
    "from layers import FullyConnectedLayer, ReLULayer\n",
    "from model import TwoLayerNet\n",
    "from trainer import Trainer, Dataset\n",
    "from optim import SGD, MomentumSGD\n",
    "from metrics import multiclass_accuracy"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Загружаем данные\n",
    "\n",
    "И разделяем их на training и validation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def prepare_for_neural_network(train_X, test_X):\n",
    "    train_flat = train_X.reshape(train_X.shape[0], -1).astype(np.float) / 255.0\n",
    "    test_flat = test_X.reshape(test_X.shape[0], -1).astype(np.float) / 255.0\n",
    "    \n",
    "    # Subtract mean\n",
    "    mean_image = np.mean(train_flat, axis = 0)\n",
    "    train_flat -= mean_image\n",
    "    test_flat -= mean_image\n",
    "    \n",
    "    return train_flat, test_flat\n",
    "    \n",
    "train_X, train_y, test_X, test_y = load_svhn(\"data\", max_train=10000, max_test=1000)    \n",
    "train_X, test_X = prepare_for_neural_network(train_X, test_X)\n",
    "# Split train into train and val\n",
    "train_X, train_y, val_X, val_y = random_split_train_val(train_X, train_y, num_val = 1000)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Как всегда, начинаем с кирпичиков\n",
    "\n",
    "Мы будем реализовывать необходимые нам слои по очереди. Каждый слой должен реализовать:\n",
    "- прямой проход (forward pass), который генерирует выход слоя по входу и запоминает необходимые данные\n",
    "- обратный проход (backward pass), который получает градиент по выходу слоя и вычисляет градиент по входу и по параметрам\n",
    "\n",
    "Начнем с ReLU, у которого параметров нет."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": false
   },
   "outputs": [],
   "source": [
    "# TODO: Implement ReLULayer layer in layers.py\n",
    "# Note: you'll need to copy implementation of the gradient_check function from the previous assignment\n",
    "\n",
    "X = np.array([[1,-2,3],\n",
    "              [-1, 2, 0.1]\n",
    "              ])\n",
    "\n",
    "assert check_layer_gradient(ReLULayer(), X)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "А теперь реализуем полносвязный слой (fully connected layer), у которого будет два массива параметров: W (weights) и B (bias).\n",
    "\n",
    "Все параметры наши слои будут использовать для параметров специальный класс `Param`, в котором будут храниться значения параметров и градиенты этих параметров, вычисляемые во время обратного прохода.\n",
    "\n",
    "Это даст возможность аккумулировать (суммировать) градиенты из разных частей функции потерь, например, из cross-entropy loss и regularization loss."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# TODO: Implement FullyConnected layer forward and backward methods\n",
    "assert check_layer_gradient(FullyConnectedLayer(3, 4), X)\n",
    "# TODO: Implement storing gradients for W and B\n",
    "assert check_layer_param_gradient(FullyConnectedLayer(3, 4), X, 'W')\n",
    "assert check_layer_param_gradient(FullyConnectedLayer(3, 4), X, 'B')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Создаем нейронную сеть\n",
    "\n",
    "Теперь мы реализуем простейшую нейронную сеть с двумя полносвязным слоями и нелинейностью ReLU. Реализуйте функцию `compute_loss_and_gradients`, она должна запустить прямой и обратный проход через оба слоя для вычисления градиентов.\n",
    "\n",
    "Не забудьте реализовать очистку градиентов в начале функции."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# TODO: In model.py, implement compute_loss_and_gradients function\n",
    "model = TwoLayerNet(n_input = train_X.shape[1], n_output = 10, hidden_layer_size = 3, reg = 0)\n",
    "loss = model.compute_loss_and_gradients(train_X[:2], train_y[:2])\n",
    "\n",
    "# TODO Now implement backward pass and aggregate all of the params\n",
    "check_model_gradient(model, train_X[:2], train_y[:2])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь добавьте к модели регуляризацию - она должна прибавляться к loss и делать свой вклад в градиенты."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# TODO Now implement l2 regularization in the forward and backward pass\n",
    "model_with_reg = TwoLayerNet(n_input = train_X.shape[1], n_output = 10, hidden_layer_size = 3, reg = 1e1)\n",
    "loss_with_reg = model_with_reg.compute_loss_and_gradients(train_X[:2], train_y[:2])\n",
    "assert loss_with_reg > loss and not np.isclose(loss_with_reg, loss), \\\n",
    "    \"Loss with regularization (%2.4f) should be higher than without it (%2.4f)!\" % (loss, loss_with_reg)\n",
    "\n",
    "check_model_gradient(model_with_reg, train_X[:2], train_y[:2])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Также реализуем функцию предсказания (вычисления значения) модели на новых данных.\n",
    "\n",
    "Какое значение точности мы ожидаем увидеть до начала тренировки?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Finally, implement predict function!\n",
    "\n",
    "# TODO: Implement predict function\n",
    "# What would be the value we expect?\n",
    "multiclass_accuracy(model_with_reg.predict(train_X[:30]), train_y[:30]) "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Допишем код для процесса тренировки\n",
    "\n",
    "Если все реализовано корректно, значение функции ошибки должно уменьшаться с каждой эпохой, пусть и медленно. Не беспокойтесь пока про validation accuracy."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "model = TwoLayerNet(n_input = train_X.shape[1], n_output = 10, hidden_layer_size = 100, reg = 1e1)\n",
    "dataset = Dataset(train_X, train_y, val_X, val_y)\n",
    "trainer = Trainer(model, dataset, SGD(), learning_rate = 1e-2)\n",
    "\n",
    "# TODO Implement missing pieces in Trainer.fit function\n",
    "# You should expect loss to go down every epoch, even if it's slow\n",
    "loss_history, train_history, val_history = trainer.fit()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.plot(train_history)\n",
    "plt.plot(val_history)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Улучшаем процесс тренировки\n",
    "\n",
    "Мы реализуем несколько ключевых оптимизаций, необходимых для тренировки современных нейросетей."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Уменьшение скорости обучения (learning rate decay)\n",
    "\n",
    "Одна из необходимых оптимизаций во время тренировки нейронных сетей - постепенное уменьшение скорости обучения по мере тренировки.\n",
    "\n",
    "Один из стандартных методов - уменьшение скорости обучения (learning rate) каждые N эпох на коэффициент d (часто называемый decay). Значения N и d, как всегда, являются гиперпараметрами и должны подбираться на основе эффективности на проверочных данных (validation data). \n",
    "\n",
    "В нашем случае N будет равным 1."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# TODO Implement learning rate decay inside Trainer.fit method\n",
    "# Decay should happen once per epoch\n",
    "\n",
    "model = TwoLayerNet(n_input = train_X.shape[1], n_output = 10, hidden_layer_size = 100, reg = 1e-1)\n",
    "dataset = Dataset(train_X, train_y, val_X, val_y)\n",
    "trainer = Trainer(model, dataset, SGD(), learning_rate_decay=0.99)\n",
    "\n",
    "initial_learning_rate = trainer.learning_rate\n",
    "loss_history, train_history, val_history = trainer.fit()\n",
    "\n",
    "assert trainer.learning_rate < initial_learning_rate, \"Learning rate should've been reduced\"\n",
    "assert trainer.learning_rate > 0.5*initial_learning_rate, \"Learning rate shouldn'tve been reduced that much!\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Накопление импульса (Momentum SGD)\n",
    "\n",
    "Другой большой класс оптимизаций - использование более эффективных методов градиентного спуска. Мы реализуем один из них - накопление импульса (Momentum SGD).\n",
    "\n",
    "Этот метод хранит скорость движения, использует градиент для ее изменения на каждом шаге, и изменяет веса пропорционально значению скорости.\n",
    "(Физическая аналогия: Вместо скорости градиенты теперь будут задавать ускорение, но будет присутствовать сила трения.)\n",
    "\n",
    "```\n",
    "velocity = momentum * velocity - learning_rate * gradient \n",
    "w = w + velocity\n",
    "```\n",
    "\n",
    "`momentum` здесь коэффициент затухания, который тоже является гиперпараметром (к счастью, для него часто есть хорошее значение по умолчанию, типичный диапазон -- 0.8-0.99).\n",
    "\n",
    "Несколько полезных ссылок, где метод разбирается более подробно:  \n",
    "http://cs231n.github.io/neural-networks-3/#sgd  \n",
    "https://distill.pub/2017/momentum/"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# TODO: Implement MomentumSGD.update function in optim.py\n",
    "\n",
    "model = TwoLayerNet(n_input = train_X.shape[1], n_output = 10, hidden_layer_size = 100, reg = 1e-1)\n",
    "dataset = Dataset(train_X, train_y, val_X, val_y)\n",
    "trainer = Trainer(model, dataset, MomentumSGD(), learning_rate=1e-4, learning_rate_decay=0.99)\n",
    "\n",
    "# You should see even better results than before!\n",
    "loss_history, train_history, val_history = trainer.fit()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Ну что, давайте уже тренировать сеть!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Последний тест - переобучимся (overfit) на маленьком наборе данных\n",
    "\n",
    "Хороший способ проверить, все ли реализовано корректно - переобучить сеть на маленьком наборе данных.  \n",
    "Наша модель обладает достаточной мощностью, чтобы приблизить маленький набор данных идеально, поэтому мы ожидаем, что на нем мы быстро дойдем до 100% точности на тренировочном наборе. \n",
    "\n",
    "Если этого не происходит, то где-то была допущена ошибка!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data_size = 15\n",
    "model = TwoLayerNet(n_input = train_X.shape[1], n_output = 10, hidden_layer_size = 100, reg = 1e-1)\n",
    "dataset = Dataset(train_X[:data_size], train_y[:data_size], val_X[:data_size], val_y[:data_size])\n",
    "trainer = Trainer(model, dataset, SGD(), learning_rate=1e-1, num_epochs=150, batch_size=5)\n",
    "\n",
    "# You should expect this to reach 1.0 training accuracy \n",
    "loss_history, train_history, val_history = trainer.fit()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь найдем гипепараметры, для которых этот процесс сходится быстрее.\n",
    "Если все реализовано корректно, то существуют параметры, при которых процесс сходится в **20** эпох или еще быстрее.\n",
    "Найдите их!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Now, tweak some hyper parameters and make it train to 1.0 accuracy in 20 epochs or less\n",
    "\n",
    "model = TwoLayerNet(n_input = train_X.shape[1], n_output = 10, hidden_layer_size = 100, reg = 1e-1)\n",
    "dataset = Dataset(train_X[:data_size], train_y[:data_size], val_X[:data_size], val_y[:data_size])\n",
    "# TODO: Change any hyperparamers or optimizators to reach training accuracy in 20 epochs\n",
    "trainer = Trainer(model, dataset, SGD(), learning_rate=1e-1, num_epochs=20, batch_size=5)\n",
    "\n",
    "loss_history, train_history, val_history = trainer.fit()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Итак, основное мероприятие!\n",
    "\n",
    "Натренируйте лучшую нейросеть! Можно добавлять и изменять параметры, менять количество нейронов в слоях сети и как угодно экспериментировать. \n",
    "\n",
    "Добейтесь точности лучше **60%** на validation set."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "# Let's train the best one-hidden-layer network we can\n",
    "\n",
    "learning_rates = 1e-4\n",
    "reg_strength = 1e-3\n",
    "learning_rate_decay = 0.999\n",
    "hidden_layer_size = 128\n",
    "num_epochs = 200\n",
    "batch_size = 64\n",
    "\n",
    "best_classifier = None\n",
    "best_val_accuracy = None\n",
    "\n",
    "loss_history = []\n",
    "train_history = []\n",
    "val_history = []\n",
    "\n",
    "# TODO find the best hyperparameters to train the network\n",
    "# Don't hesitate to add new values to the arrays above, perform experiments, use any tricks you want\n",
    "# You should expect to get to at least 40% of valudation accuracy\n",
    "# Save loss/train/history of the best classifier to the variables above\n",
    "\n",
    "print('best validation accuracy achieved: %f' % best_val_accuracy)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(15, 7))\n",
    "plt.subplot(211)\n",
    "plt.title(\"Loss\")\n",
    "plt.plot(loss_history)\n",
    "plt.subplot(212)\n",
    "plt.title(\"Train/validation accuracy\")\n",
    "plt.plot(train_history)\n",
    "plt.plot(val_history)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Как обычно, посмотрим, как наша лучшая модель работает на тестовых данных"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "test_pred = best_classifier.predict(test_X)\n",
    "test_accuracy = multiclass_accuracy(test_pred, test_y)\n",
    "print('Neural net test set accuracy: %f' % (test_accuracy, ))"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
